package cn.asiontang.xposed.url_firewall;

import android.util.SparseArray;

import java.net.InetAddress;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import cn.asiontang.xposed.LogEx;
import cn.asiontang.xposed.XposedUtils;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class XposedHookClass_Socket
{
    private static final String TAG = XposedHookClass_Socket.class.getName();

    /**
     * <pre>
     * 此方法优点:
     *      1.能同时捕获 ApacheHttp , HttpURLConnection发起的请求
     * 此方法缺点:
     *      1.捕获 HttpURLConnection 发起的请求时会丢失URL # 之后的部分.
     *      2.不支持所有 HTTPS 协议的请求.
     * </pre>
     * <p><b>测试用例如下:</b></p>
     * <pre>
     * //测试 HTTP 协议
     * testApacheHttp("http://www.baidu.com/search/error.html?Http.Apache#this_ref_will_not_lost");
     * testHttpURLConnection("http://www.baidu.com/search/error.html?Http.URLConnection#this_ref_will_lost");//#后面的部分会丢失.
     *
     * //测试 HTTPS 协议
     * testApacheHttp("https://www.baidu.com/search/error.html?Https.Apache#this_ref_will_not_lost");
     * testHttpURLConnection("https://www.baidu.com/search/error.html?Https.URLConnection#this_ref_will_lost");
     * </pre>
     */
    public static void hook(final XC_LoadPackage.LoadPackageParam loadPackageParam)
    {
        try
        {
            //TODO:当是整个IP,域名直接禁止模式,可以在此禁止解析也能达到禁止连接的目的.
            //获取请求的IP与域名的关系
            final Map<String, String> mIpAndDomainMap = new HashMap<String, String>();
            /**
             * 将当前输出流对应的实例 与 当前正在进行连接的IP和端口 绑定起来,供 获取完整URL的地方获取数据.
             */
            final SparseArray<String> mOutputStreamInstanceAndIpPortMap = new SparseArray<String>();

            XC_MethodHook methodHook_getByName = new XC_MethodHook()
            {
                @Override
                protected void afterHookedMethod(final XC_MethodHook.MethodHookParam param) throws Throwable
                {
                    String domain = (String) param.args[0];
                    InetAddress address = (InetAddress) param.getResult();
                    String ip = address.getHostAddress();

                    //当域名本来传的值本来就是IP时,得到的结果也是相同IP,所以可以排除掉.
                    if (domain.equals(ip))
                        return;
                    mIpAndDomainMap.put(ip, domain);
                }
            };
            XC_MethodHook methodHook_getAllByName = new XC_MethodHook()
            {
                @Override
                protected void afterHookedMethod(final XC_MethodHook.MethodHookParam param) throws Throwable
                {
                    String domain = (String) param.args[0];
                    for (InetAddress address : (InetAddress[]) param.getResult())
                    {
                        String ip = address.getHostAddress();

                        //当域名本来传的值本来就是IP时,得到的结果也是相同IP,所以可以排除掉.
                        if (domain.equals(ip))
                            return;
                        mIpAndDomainMap.put(ip, domain);
                    }
                }
            };
            XposedUtils.realHookAllMethods("java.net.InetAddress", loadPackageParam.classLoader, "getByName", methodHook_getByName);
            XposedUtils.realHookAllMethods("java.net.InetAddress", loadPackageParam.classLoader, "getByNameOnNet", methodHook_getByName);
            XposedUtils.realHookAllMethods("java.net.InetAddress", loadPackageParam.classLoader, "getAllByName", methodHook_getAllByName);
            XposedUtils.realHookAllMethods("java.net.InetAddress", loadPackageParam.classLoader, "getAllByNameOnNet", methodHook_getAllByName);

            //获取具体请求的网址
            XposedUtils.realHookAllMethods("java.net.Socket", loadPackageParam.classLoader, "getOutputStream", new XC_MethodHook()
            {
                Set<Unhook> XC_MethodHook_OutputStream_writeUnHook;

                @Override
                protected void afterHookedMethod(final MethodHookParam param) throws Throwable
                {
                    Socket socket = (Socket) param.thisObject;

                    //通过IP拿到对应的网址.
                    String ipOrDomain = socket.getInetAddress().getHostAddress();
                    if (mIpAndDomainMap.containsKey(ipOrDomain))
                        ipOrDomain = mIpAndDomainMap.get(ipOrDomain);

                    //拼接正确的端口
                    int port = socket.getPort();
                    String portString = port == 80 || port == 443 ? "" : ":" + port;

                    //拼接正确的协议
                    String protocol = port == 443 ? "https" : "http";

                    //将当前输出流对应的实例 与 当前正在进行连接的IP和端口 绑定起来,供 获取完整URL的地方获取数据.
                    //protocol://username:password@host:port/path?query#ref
                    mOutputStreamInstanceAndIpPortMap.put(param.getResult().hashCode(), String.format("%s://%s%s", protocol, ipOrDomain, portString));

                    //Hook住 PlainSocketImpl$PlainSocketOutputStream 对象的 write 方法
                    if (XC_MethodHook_OutputStream_writeUnHook == null)
                        XC_MethodHook_OutputStream_writeUnHook = XposedBridge.hookAllMethods(param.getResult().getClass(), "write", new XC_MethodHook()
                        {
                            private Pattern mUrlPattern;

                            /**
                             * Socket 的 OutputStream 的 write 方法
                             * 参考源码:
                             * <ul>
                             * <li><a href="http://osxr.org:8080/android/source/libcore/luni/src/main/java/java/net/PlainSocketImpl.java">android-4.1.2_r2/libcore/luni/src/main/java/java/net/PlainSocketImpl.java</a></li>
                             * </ul>
                             */
                            @Override
                            protected void afterHookedMethod(final MethodHookParam param) throws Throwable
                            {
                                if (param.args == null)
                                {
                                    LogEx.log(TAG, "异常:理论上OutputStream的write方法参数不会为空!");
                                    return;
                                }

                                String cmd;
                                switch (param.args.length)
                                {
                                    case 1:
                                    {
                                        //void write(byte[] buffer)
                                        if (param.args[0] instanceof Byte[])
                                        {
                                            byte[] buffer = (byte[]) param.args[0];

                                            cmd = new String(buffer);
                                        }
                                        //void write(int oneByte)
                                        else if (param.args[0] instanceof Integer)
                                        {
                                            cmd = String.valueOf(param.args[0]);
                                        }
                                        else
                                        {
                                            LogEx.log(TAG, "异常:出现了理论上OutputStream的write方法没有的参数:" + LogEx.getDebugString(param.args));
                                            return;
                                        }
                                    }
                                    break;
                                    case 3:
                                    {
                                        //void write(byte[] buffer, int offset, int count)

                                        byte[] buffer = (byte[]) param.args[0];
                                        int byteOffset = (Integer) param.args[1];
                                        int byteCount = (Integer) param.args[2];

                                        cmd = new String(buffer, byteOffset, byteCount);
                                    }
                                    break;
                                    default:
                                    {
                                        LogEx.log(TAG, "异常:未知的OutputStream的write方法参数个数" + param.args.length, LogEx.getDebugString(param.args));
                                        return;
                                    }
                                }

                                //特殊情况下,如二进制流时,会出现找不到\n的情况.
                                int lastNewLineIndex = cmd.indexOf("\n") - 1;
                                if (lastNewLineIndex <= 0)
                                {
                                    LogEx.log(TAG, "异常:未知的OutputStream的write数据:"
                                            , "lastNewLineIndex <= 0"
                                            , "cmd=", cmd, "cmd.length()=", cmd.length()
                                            , "param.thisObject=", param.thisObject, "param.method=", param.method);
                                    return;
                                }

                                //===================================================================================
                                // cmd 输出例子:
                                //    GET / HTTP/1.1
                                //    POST /?testHttpURLConnection HTTP/1.1
                                //    POST /error.jsp?traceid=147340263107016350828006088070015794472 HTTP/1.1
                                //===================================================================================
                                final Matcher m = getUrlPattern().matcher(cmd.substring(0, lastNewLineIndex));
                                if (!m.matches() || m.groupCount() < 1)
                                {
                                    LogEx.log(TAG, "异常:未知的OutputStream的write数据,无法正确提取URL:"
                                            , "getUrlPattern().matcher"
                                            , "cmd=", cmd, "cmd.length()=", cmd.length()
                                            , "m", m);
                                    return;
                                }

                                String ipAndPort = mOutputStreamInstanceAndIpPortMap.get(param.thisObject.hashCode());
                                String path = m.group(1);

                                LogEx.log(TAG, "成功捕获网址", "Socket", ipAndPort + path, m.group());

                                //将已经正确取出来的资源移除掉.
                                mOutputStreamInstanceAndIpPortMap.remove(param.thisObject.hashCode());

                                //不允许继续执行的情况.
                                //param.setThrowable(new SocketTimeoutException());
                            }

                            private Pattern getUrlPattern()
                            {
                                if (mUrlPattern == null)
                                    mUrlPattern = Pattern.compile("^[^ ]+ (.+) HTTP/\\d\\.\\d");
                                return mUrlPattern;
                            }
                        });
                }
            });
        }
        catch (Exception e)
        {
            LogEx.log(TAG, "hook Exception", e);
        }
    }
}
